package com.milhouz.wormdroid.gl.view;

import java.util.ArrayList;
import java.util.concurrent.Semaphore;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGL11;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL;
import javax.microedition.khronos.opengles.GL10;

import simpull.Group;
import simpull.Simpull;
import simpull.Vector2f;
import simpull.VectorForce;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.Toast;

import com.milhouz.wormdroid.data.model.GameData;
import com.milhouz.wormdroid.gl.factory.WeaponFactory;
import com.milhouz.wormdroid.gl.model.BotGLSprite;
import com.milhouz.wormdroid.gl.model.GLSprite;
import com.milhouz.wormdroid.gl.model.NoPhysicGLSprite;
import com.milhouz.wormdroid.gl.model.TargetGLSprite;
import com.milhouz.wormdroid.gl.model.WeaponGLSprite;
import com.milhouz.wormdroid.gl.model.hud.HudSpriteNames;
import com.milhouz.wormdroid.util.ProfileRecorder;

/**
 * An implementation of SurfaceView that uses the dedicated surface for displaying an OpenGL animation. This allows the animation to run in a separate thread, without requiring that it be driven by
 * the update mechanism of the view hierarchy.
 * 
 * The application-specific rendering code is delegated to a GLView.Renderer instance.
 */
public class GameGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

	public Group defaultPhysicsGroup = new Group(true);
	
	// GAME DATA
	private GameData mGameData;
	private TargetGLSprite mCurrentBotTarget;
	public boolean mIsTargetDisplayed = false;
	
	// SCREEN VIEW DATA
	private int xval;
	private int yval;
	private int zx;
	private int zy;
	public boolean isViewMoved = false;
	
	public GameGLSurfaceView(Context context) {
		super(context);
		init();
	}

	public GameGLSurfaceView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	private void init() {
		// Install a SurfaceHolder.Callback so we get notified when the
		// underlying surface is created and destroyed
		mHolder = getHolder();
		mHolder.addCallback(this);
		mHolder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
		setFocusableInTouchMode(true);
	}

	public void addSpriteToPhysicEngine(GLSprite sprite) {
		if (!(sprite instanceof NoPhysicGLSprite))
			defaultPhysicsGroup.add(sprite);
	}

	public void addSpritesToPhysicEngine(ArrayList<GLSprite> sprites) {
		for (GLSprite sprite : sprites) {
			if (!(sprite instanceof NoPhysicGLSprite))
				defaultPhysicsGroup.add(sprite);
		}
	}

	public SurfaceHolder getSurfaceHolder() {
		return mHolder;
	}

	public void setGLWrapper(GLWrapper glWrapper) {
		mGLWrapper = glWrapper;
	}

	public void setRenderer(Renderer renderer) {
		mGLThread = new GLThread(renderer);
		mGLThread.start();
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent msg) {
		if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
			getSelectedBot().moveRight(mCurrentBotTarget);
		}
		if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
			getSelectedBot().moveLeft(mCurrentBotTarget);
		}
		if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
			if (mCurrentBotTarget != null && !mCurrentBotTarget.isLaunched) {
				int botX = (int) getSelectedBot().getX();
				int botY = (int) getSelectedBot().getY();
				mCurrentBotTarget.moveUpWeaponTarget(botX, botY);
			} else {
				getSelectedBot().jump();
			}
		}
		if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
			if (mCurrentBotTarget != null && !mCurrentBotTarget.isLaunched) {
				int botX = (int) getSelectedBot().getX();
				int botY = (int) getSelectedBot().getY();
				mCurrentBotTarget.moveDownWeaponTarget(botX, botY);
			}
		}
		if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
			if (mCurrentBotTarget != null && !mCurrentBotTarget.isLaunched) {
				mCurrentBotTarget.launchWeapon();
				// TODO Add launch to engine : addSpriteToPhysicEngine(mCurrentBotTarget);
			}
		}
		return super.onKeyDown(keyCode, msg);
	}

	public void onSelectMenuWeapon() {
		if (mCurrentBotTarget != null && !mCurrentBotTarget.isLaunched) {
			defaultPhysicsGroup.remove(mCurrentBotTarget);
			mGLThread.mRenderer.removeSprite(mCurrentBotTarget);
			mCurrentBotTarget = null;
		} else {
			DialogTestView dialog = new DialogTestView(getContext());
			dialog.show();
			mIsTargetDisplayed = true;
			int botX = (int) getSelectedBot().getX();
			int botY = (int) getSelectedBot().getY();
			mCurrentBotTarget = WeaponFactory.getInstance().getTargetSprite(botX, botY);
			mCurrentBotTarget.isHide = false;
		}
	}

	public void onSelectMenuView() {
		if (getSelectedBot().isViewCentered) {
			isViewMoved = true;
			getSelectedBot().isViewCentered = false;
		} else {
			mGLThread.mRenderer.centerCameraOnSprite(getSelectedBot());
			isViewMoved = false;
			getSelectedBot().isViewCentered = true;
		}
	}

	public void onSelectMenuZoom() {
		mGLThread.mRenderer.setZoom();
	}
	
	@Override
	public boolean onTrackballEvent(MotionEvent event) {
		int action = event.getAction();
		if (action == MotionEvent.ACTION_MOVE) {
			if (event.getY() < 0) {
				// Up
				if (mCurrentBotTarget != null && !mCurrentBotTarget.isLaunched) {
					int botX = (int) getSelectedBot().getX();
					int botY = (int) getSelectedBot().getY();
					mCurrentBotTarget.moveUpWeaponTarget(botX, botY);
				} else {
					getSelectedBot().jump();
				}
				return true;
			} else if (event.getY() > 0) {
				// Down
				if (mCurrentBotTarget != null && !mCurrentBotTarget.isLaunched) {
					int botX = (int) getSelectedBot().getX();
					int botY = (int) getSelectedBot().getY();
					mCurrentBotTarget.moveDownWeaponTarget(botX, botY);
				}
			} else	if (event.getX() >= 0) {
				// Right
				getSelectedBot().moveRight(mCurrentBotTarget);
				return true;
			} else if (event.getX() < 0) {
				// Left
				getSelectedBot().moveLeft(mCurrentBotTarget);
				return true;
			}
		}
		return super.onTrackballEvent(event);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO changer en mettant le current sprite
		if (!getSelectedBot().isViewCentered) {
			int action = event.getAction();
			if (action == MotionEvent.ACTION_DOWN) {
				xval = (int) event.getX();
				yval = (int) event.getY();
				isViewMoved = true;
			}
			if (action == MotionEvent.ACTION_UP) {
				int xnow = (int) event.getX();
				int ynow = (int) event.getY();
				if (xnow < 20 && ynow < 20) {
					zx = 0;
					zy = 0;
					isViewMoved = true;
				}
				if (event.getDownTime() >= 3000) {
					Toast.makeText(getContext(), "Weapon", Toast.LENGTH_LONG);
				}
			}
			if (action == MotionEvent.ACTION_MOVE) {
				zx = zx + ((int) event.getX() - xval);
				zy = zy + ((int) event.getY() - yval);
				xval = (int) event.getX();
				yval = (int) event.getY();
				isViewMoved = true;
			}
		}
		return isViewMoved;
	}

	public void surfaceCreated(SurfaceHolder holder) {
		mGLThread.surfaceCreated();
	}

	public void surfaceDestroyed(SurfaceHolder holder) {
		// Surface will be destroyed when we return
		mGLThread.surfaceDestroyed();
	}

	public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
		// Surface size or format has changed.
		mGLThread.onWindowResize(w, h);
	}

	/**
	 * Inform the view that the activity is paused.
	 */
	public void onPause() {
		mGLThread.onPause();
	}

	/**
	 * Inform the view that the activity is resumed.
	 */
	public void onResume() {
		mGLThread.onResume();
	}

	/**
	 * Inform the view that the window focus has changed.
	 */
	@Override
	public void onWindowFocusChanged(boolean hasFocus) {
		super.onWindowFocusChanged(hasFocus);
		mGLThread.onWindowFocusChanged(hasFocus);
	}

	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		mGLThread.requestExitAndWait();
	}

	// ----------------------------------------------------------------------

	public void setGameData(GameData gameData) {
		this.mGameData = gameData;
	}

	public GameData getGameData() {
		return mGameData;
	}
	
	public BotGLSprite getSelectedBot() {
		BotGLSprite selBot = (BotGLSprite) mGLThread.mRenderer.getSpriteById(mGameData.currentBotId);
		selBot.isSelectedBot = true;
		return selBot;
	}
	
	public WeaponGLSprite getSelectedWeapon() {
		WeaponGLSprite selWeapon = (WeaponGLSprite) mGLThread.mRenderer.getSpriteById(mGameData.currentBotId);
		return selWeapon;
	}

	public interface GLWrapper {
		GL wrap(GL gl);
	}

	// ----------------------------------------------------------------------

	/**
	 * A generic renderer interface.
	 */
	public interface Renderer {

		/**
		 * @return the EGL configuration specification desired by the renderer.
		 */
		int[] getConfigSpec();

		/**
		 * Surface created. Called when the surface is created. Called when the application starts, and whenever the GPU is reinitialized. This will typically happen when the device awakes after going to
		 * sleep. Set your textures here.
		 */
		void surfaceCreated(GL10 gl);

		/**
		 * Called when the rendering thread is about to shut down. This is a good place to release OpenGL ES resources (textures, buffers, etc).
		 * 
		 * @param gl
		 */
		void shutdown(GL10 gl);

		/**
		 * Surface changed size. Called after the surface is created and whenever the OpenGL ES surface size changes. Set your viewport here.
		 * 
		 * @param gl
		 * @param width
		 * @param height
		 */
		void sizeChanged(GL10 gl, int width, int height);

		/**
		 * Draw the current frame.
		 * 
		 * @param gl
		 */
		void drawFrame(GL10 gl, int width, int height);

		void moveCamera(int dx, int dy);

		void moveCameraWithSprite(GLSprite sprite);

		void centerCameraOnSprite(GLSprite sprite);

		GLSprite getSpriteById(int idSprite);
		
		ArrayList<GLSprite> getSprites();
		
		void addAndLoadSprite(GL10 gl, GLSprite sprite);
		
		void removeSprite(GLSprite sprite);
		
		void setZoom();
		
		boolean isZoomed();
	}

	/**
	 * An EGL helper class.
	 */

	private class EglHelper {
		public EglHelper() {

		}

		/**
		 * Initialize EGL for a given configuration spec.
		 * 
		 * @param configSpec
		 */
		public void start(int[] configSpec) {
			/*
			 * Get an EGL instance
			 */
			mEgl = (EGL10) EGLContext.getEGL();

			/*
			 * Get to the default display.
			 */
			mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

			/*
			 * We can now initialize EGL for that display
			 */
			int[] version = new int[2];
			mEgl.eglInitialize(mEglDisplay, version);

			EGLConfig[] configs = new EGLConfig[1];
			int[] num_config = new int[1];
			mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, num_config);
			mEglConfig = configs[0];

			/*
			 * Create an OpenGL ES context. This must be done only once, an OpenGL context is a somewhat heavy object.
			 */
			mEglContext = mEgl.eglCreateContext(mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, null);

			mEglSurface = null;
		}

		/*
		 * Create and return an OpenGL surface
		 */
		public GL createSurface(SurfaceHolder holder) {
			/*
			 * The window size has changed, so we need to create a new surface.
			 */
			if (mEglSurface != null) {

				/*
				 * Unbind and destroy the old EGL surface, if there is one.
				 */
				mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
				mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
			}

			/*
			 * Create an EGL surface we can render into.
			 */
			mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, holder, null);

			/*
			 * Before we can issue GL commands, we need to make sure the context is current and bound to a surface.
			 */
			mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);

			GL gl = mEglContext.getGL();
			if (mGLWrapper != null) {
				gl = mGLWrapper.wrap(gl);
			}
			return gl;
		}

		/**
		 * Display the current render surface.
		 * 
		 * @return false if the context has been lost.
		 */
		public boolean swap() {
			mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);

			/*
			 * Always check for EGL_CONTEXT_LOST, which means the context and all associated data were lost (For instance because the device went to sleep). We need to sleep until we get a new surface.
			 */
			return mEgl.eglGetError() != EGL11.EGL_CONTEXT_LOST;
		}

		public void finish() {
			if (mEglSurface != null) {
				mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
				mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
				mEglSurface = null;
			}
			if (mEglContext != null) {
				mEgl.eglDestroyContext(mEglDisplay, mEglContext);
				mEglContext = null;
			}
			if (mEglDisplay != null) {
				mEgl.eglTerminate(mEglDisplay);
				mEglDisplay = null;
			}
		}

		EGL10 mEgl;
		EGLDisplay mEglDisplay;
		EGLSurface mEglSurface;
		EGLConfig mEglConfig;
		EGLContext mEglContext;
	}

	/**
	 * A generic GL Thread. Takes care of initializing EGL and GL. Delegates to a Renderer instance to do the actual drawing.
	 * 
	 */

	class GLThread extends Thread {
		GLThread(Renderer renderer) {
			super();
			mDone = false;
			mWidth = 0;
			mHeight = 0;
			mRenderer = renderer;
			setName("GLThread");
		}

		private void initPhysicEngine() {
			// Initialisation du moteur (ajout de l'engine step time)
			Simpull.init(3/4f);
			// Ajoute une force globale sur tous les objets
			Simpull.add(new VectorForce(false, 0.0f, -2f));
			Simpull.add(defaultPhysicsGroup);
		}

		@Override
		public void run() {
			/*
			 * When the android framework launches a second instance of an activity, the new instance's onCreate() method may be called before the first instance returns from onDestroy().
			 * 
			 * This semaphore ensures that only one instance at a time accesses EGL.
			 */
			try {
				try {
					sEglSemaphore.acquire();
				} catch (InterruptedException e) {
					return;
				}
				guardedRun();
			} catch (InterruptedException e) {
				// fall thru and exit normally
			} finally {
				sEglSemaphore.release();
			}
		}

		private void guardedRun() throws InterruptedException {
			initPhysicEngine();
			mEglHelper = new EglHelper();
			/*
			 * Specify a configuration for our opengl session and grab the first configuration that matches is
			 */
			int[] configSpec = mRenderer.getConfigSpec();
			mEglHelper.start(configSpec);

			GL10 gl = null;
			boolean tellRendererSurfaceCreated = true;
			boolean tellRendererSurfaceChanged = true;

			/*
			 * This is our main activity thread's loop, we go until asked to quit.
			 */
			while (!mDone) {
				ProfileRecorder.sSingleton.start(ProfileRecorder.PROFILE_FRAME);
				/*
				 * Update the asynchronous state (window size)
				 */
				int w, h;
				boolean changed;
				boolean needStart = false;
				synchronized (this) {
					ProfileRecorder.sSingleton.start(ProfileRecorder.PROFILE_SIM);
					Simpull.step();
					ProfileRecorder.sSingleton.stop(ProfileRecorder.PROFILE_SIM);
					if (mPaused) {
						mEglHelper.finish();
						needStart = true;
					}
					if (needToWait()) {
						while (needToWait()) {
							wait();
						}
					}
					if (mDone) {
						break;
					}
					changed = mSizeChanged;
					w = mWidth;
					h = mHeight;
					mSizeChanged = false;
				}
				if (needStart) {
					mEglHelper.start(configSpec);
					tellRendererSurfaceCreated = true;
					changed = true;
				}
				if (changed) {
					gl = (GL10) mEglHelper.createSurface(mHolder);
					tellRendererSurfaceChanged = true;
				}
				if (tellRendererSurfaceCreated) {
					mRenderer.surfaceCreated(gl);
					tellRendererSurfaceCreated = false;
				}
				if (tellRendererSurfaceChanged) {
					mRenderer.sizeChanged(gl, w, h);
					tellRendererSurfaceChanged = false;
				}
				if ((w > 0) && (h > 0)) {
					ProfileRecorder.sSingleton.start(ProfileRecorder.PROFILE_DRAW);
					// Round time over 
					if (mGameData.isEndOfRound()) {
						// Change selected bot
						getSelectedBot().isViewCentered = false;
						mGameData.setSelectedBotHud(((BotGLSprite)mRenderer.getSpriteById(mGameData.getNextBotId())));
					}
					// Translate the view when screen is touch
					if (isViewMoved) {
						mRenderer.moveCamera(zx, zy);
						isViewMoved = false;
					} else {
						// Center the camera to the current sprite
						mRenderer.moveCameraWithSprite(getSelectedBot());
						if (mCurrentBotTarget != null) {
							mCurrentBotTarget.centerTargetToCoordonates((int) getSelectedBot().getX(), (int) getSelectedBot().getY());
						}
					}
					// Display Weapon
					if (mIsTargetDisplayed) {
						mRenderer.addAndLoadSprite(gl, mCurrentBotTarget);
						mIsTargetDisplayed = false;
					}
					
					/* draw a frame here */
					mRenderer.drawFrame(gl, w, h);
					ProfileRecorder.sSingleton.stop(ProfileRecorder.PROFILE_DRAW);
					/*
					 * Once we're done with GL, we need to call swapBuffers() to instruct the system to display the rendered frame
					 */
					ProfileRecorder.sSingleton.start(ProfileRecorder.PROFILE_PAGE_FLIP);
					mEglHelper.swap();
					ProfileRecorder.sSingleton.stop(ProfileRecorder.PROFILE_PAGE_FLIP);
				}
				ProfileRecorder.sSingleton.stop(ProfileRecorder.PROFILE_FRAME);
				// Set the FPS value to the HUD
				if (mGameData != null)
					mGameData.mHud.setHudValue(HudSpriteNames.FPS_SPRITE, String.valueOf(ProfileRecorder.sSingleton.getAverageTime(ProfileRecorder.PROFILE_FRAME)));
				ProfileRecorder.sSingleton.endFrame();
			}

			/*
			 * clean-up everything...
			 */
			if (gl != null) {
				mRenderer.shutdown(gl);
			}

			mEglHelper.finish();
		}

		private boolean needToWait() {
			return (mPaused || (!mHasFocus) || (!mHasSurface) || mContextLost) && (!mDone);
		}
	
		public void surfaceCreated() {
			synchronized (this) {
				mHasSurface = true;
				mContextLost = false;
				notify();
			}
		}

		public void surfaceDestroyed() {
			synchronized (this) {
				mHasSurface = false;
				notify();
			}
		}

		public void onPause() {
			synchronized (this) {
				mPaused = true;
			}
		}

		public void onResume() {
			synchronized (this) {
				mPaused = false;
				notify();
			}
		}

		public void onWindowFocusChanged(boolean hasFocus) {
			synchronized (this) {
				mHasFocus = hasFocus;
				if (mHasFocus == true) {
					notify();
				}
			}
		}

		public void onWindowResize(int w, int h) {
			synchronized (this) {
				mWidth = w;
				mHeight = h;
				mSizeChanged = true;
			}
		}

		public void requestExitAndWait() {
			// don't call this from GLThread thread or it is a guaranteed
			// deadlock!
			synchronized (this) {
				mDone = true;
				notify();
			}
			try {
				join();
			} catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
			}
		}

		private boolean mDone;
		private boolean mPaused;
		private boolean mHasFocus;
		private boolean mHasSurface;
		private boolean mContextLost;
		private int mWidth;
		private int mHeight;
		private Renderer mRenderer;
		// TODO private Runnable mEvent; (Mettre la simulation dans son propre thread)
		private EglHelper mEglHelper;
	}

	private static final Semaphore sEglSemaphore = new Semaphore(1);
	private boolean mSizeChanged = true;

	private SurfaceHolder mHolder;
	private GLThread mGLThread;
	private GLWrapper mGLWrapper;

}
